package com.trg.flex.dt
{
	import com.trg.flex.FlexUtils;
	import com.trg.flex.dp.DataProvider;
	import com.trg.flex.dp.LocalDataProvider;
	
	import flash.display.DisplayObject;
	import flash.events.Event;
	
	import mx.binding.utils.ChangeWatcher;
	import mx.collections.CursorBookmark;
	import mx.collections.IViewCursor;
	import mx.collections.Sort;
	import mx.core.Container;
	import mx.events.CollectionEvent;
	import mx.events.ListEvent;

	[Style(name="loadingIcon",type="Class",inherited="no")]
	[Style(name="loadingErrorIcon",type="Class",inherited="no")]
	
	/**
	 * Fired when a row in this table is clicked.
	 */
	[Event(name="clickAction",type="com.trg.flex.dt.DataTableEvent")]
	
	/**
	 * Fired when a row in this table is double-clicked.
	 */
	[Event(name="doubleClickAction",type="com.trg.flex.dt.DataTableEvent")]
	
	/**
	 *  Dispatched when the <code>selectedIndex</code> or <code>selectedItem</code> property
	 *  changes as a result of user interaction.
	 *
	 *  @eventType mx.events.ListEvent.CHANGE
	 */
	[Event(name="change", type="mx.events.ListEvent")]
	
	[DefaultProperty("columns")]
	
	/**
	 * This is a wrapper for the standard DataGrid that adds a lot of features
	 * to make life easier for the developer. The core feature is probably
	 * enabling nested properties to be used for column values (for example “type.name”),
	 * but there are many other features. These include:  an option to include a
	 * selection column, an option to preserve selection when the data is refreshed,
	 * the ability to incorporate a pager, auto-sizing to results and auto-paging
	 * to fit on the screen.
	 */
	public class DataTable extends Container
	{
		public static const AUTOSIZE_NONE:String = "none";
		public static const AUTOSIZE_SIZE_FROM_RESULTS:String = "size-from-results";
		public static const AUTOSIZE_SIZE_FROM_PAGING:String = "size-from-paging";
		public static const AUTOSIZE_PAGING_FROM_SIZE:String = "paging-from-size";
		
		public function DataTable()
		{
			super();
			autoLayout = false;
			dataGrid = new ExtendedDataGrid();
			dataGrid.columns = [];
			dataGrid.addEventListener("change", relayEvent);
			dataGrid.selectable = false;
			dataGrid.percentWidth = 100;
			dataGrid.percentHeight = 100;
			dataGrid.addEventListener(ListEvent.ITEM_CLICK, itemClick);
			dataGrid.addEventListener(ListEvent.ITEM_DOUBLE_CLICK, itemClick);
			dataGrid.addEventListener(ExtendedDataGrid.AFTER_COLLECTION_CHANGE, afterCollectionChange);
			dataGrid.addEventListener("change", selectionChanged, false, 1000);
			addChild(dataGrid);
			resultsPerPageWatcher = ChangeWatcher.watch(null, "resultsPerPage", recalcAutoSizeForPaging);
			//setStyle("borderStyle","solid");
		}
		
		override public function initialize():void {
			super.initialize()
			_initialized = true;
			if (_pagedDataProvider != null)
				_pagedDataProvider.refresh();
		}
		
		protected var _noResultsMessage:String;
		protected var _loadingErrorMessage:String;
		protected var _defaultSort:Sort;
		protected var _sort:Sort;
		protected var _rawDataProvider:Object;
		protected var _pagedDataProvider:DataProvider;
		protected var _dataCursor:IViewCursor;
		protected var _columns:Array = [];
		protected var _showSelectionColumn:Boolean;
		protected var _pager:IPager;
		protected var _autoSize:String;
		
		protected var dataGrid:ExtendedDataGrid;
		protected var _initialized:Boolean = false;
		protected var _loading:Boolean;
		
		protected var selectionColumn:SelectionColumn;
		protected var resultsPerPageWatcher:ChangeWatcher;
		
	//PRIVATE FUNCTIONS
		protected function relayEvent(event:Event):void {
			dispatchEvent(event);
		}
		
		protected function loadingChanged(event:Event=null):void {
			_loading = _pagedDataProvider == null ? false : _pagedDataProvider.loading;
			dispatchEvent(new Event(DataProvider.LOADING_CHANGED));
		}
		
		protected function setDataProvider():void {
			if (_pager != null) _pager.dataProvider = _pagedDataProvider;
			recalcAutoSizeForPaging();
			recalcAutoSizeForResults();
			if (_pagedDataProvider == null) {
				_dataCursor = null;
				return;
			}
			_dataCursor = _pagedDataProvider.createCursor();
			if (_pagedDataProvider.sort == null && _sort != null)
				_pagedDataProvider.sort = _sort;
			_pagedDataProvider.addEventListener(DataProvider.LOADING_CHANGED, loadingChanged);
			resultsPerPageWatcher.reset(_pagedDataProvider);
			_pagedDataProvider.addEventListener(CollectionEvent.COLLECTION_CHANGE, recalcAutoSizeForResults);
			if (_initialized && _pagedDataProvider != null)
				_pagedDataProvider.refresh();
		}
		protected function unsetDataProvider():void {
			if (_pagedDataProvider == null) return;
			_sort = _pagedDataProvider.sort;
			_pagedDataProvider.removeEventListener(DataProvider.LOADING_CHANGED, loadingChanged);
			resultsPerPageWatcher.unwatch();
			_pagedDataProvider.removeEventListener(CollectionEvent.COLLECTION_CHANGE, recalcAutoSizeForResults);
		}

		protected function itemClick(event:ListEvent):void {
			var newEvent:DataTableEvent;
			if (event.type == ListEvent.ITEM_CLICK) {
				newEvent = new DataTableEvent(DataTableEvent.CLICK);
			} else {
				newEvent = new DataTableEvent(DataTableEvent.DOUBLE_CLICK);
			}
			var col:* = dataGrid.columns[event.columnIndex];
			if (col is Column) {
				newEvent.column = col as Column;
			}
			newEvent.item = _pagedDataProvider.getItemAt(event.rowIndex);
			if (dataGrid.allowMultipleSelection) {
				newEvent.selectedItems = dataGrid.selectedItems;
			} else {
				newEvent.selectedItem = dataGrid.selectedItem;
			}
			dispatchEvent(newEvent);
		}
		
		protected function applyColumns():void {
			var newColumns:Array = _columns;
			if (_showSelectionColumn && dataGrid.selectable) {
				dataGrid.uiSelectMode = dataGrid.allowMultipleSelection
						? ExtendedDataGrid.SELECT_MODE_MULTI : ExtendedDataGrid.SELECT_MODE_SINGLE;
				if (selectionColumn == null) {
					selectionColumn = new SelectionColumn();
				}
				selectionColumn.multiSelect = dataGrid.allowMultipleSelection;
				newColumns = [selectionColumn].concat(newColumns);
			} else {
				dataGrid.uiSelectMode = ExtendedDataGrid.SELECT_MODE_NORMAL;
				if (selectionColumn != null) {
					selectionColumn = null;
				}
			}
			if (!FlexUtils.arraysEqual(newColumns, dataGrid.columns)) {
				dataGrid.columns = newColumns;
			}
		}
		
	//PUBLIC FUNCTIONS
		/**
		 * Forces the underlying data provider to refresh.
		 */
		public function refresh():void {
			if (_pagedDataProvider != null) {
				_pagedDataProvider.refresh();
			}
		}
	
	//PUBLIC STATE PROPERTIES
		/**
		 * 
		 */
		[Bindable(event="loadingChanged")]
		public function get loading():Boolean {
			return _loading;
		}
		
	//GETTERS & SETTERS
		/**
		 * If “single”, one row can be selected at a time. If “multi” any number of rows
		 * can be selected simultaneously. If “none”, no rows can be selected.
		 */
		[Inspectable(enumeration="single,multi,none", defaultValue="none")]
		[Bindable]
		public function get selectable():String {
			if (dataGrid.selectable) {
				if (dataGrid.allowMultipleSelection) {
					return "multi";
				} else {
					return "single";
				}
			} else {
				return "none";
			}
		}
		public function set selectable(value:String):void {
			if (value == "single" || value == "multi") {
				if (_showSelectionColumn && ((value == "single" && selectable == "multi")
						|| (value == "multi" && selectable == "single"))) {
					//force renderer refresh by removing the column temporarily when
					//switching between multi and single
					dataGrid.columns = _columns;
				}
				dataGrid.selectable = true;
				dataGrid.allowMultipleSelection = (value == "multi");
				if (value == "single" && selectedItems != null && selectedItems.length > 1) {
					selectedItem = selectedItems[0];	
				}
			} else {
				dataGrid.selectable = false;
				selectedItem = null;
			}
			applyColumns();
		}
		
		/**
		 * Show a selection column on the left side of the table. It will use check boxes
		 * or radio buttons depending on whether single- or multi- select is enabled. If 
		 * selectable is “none”, the column will not be displayed.
		 */
		[Inspectable]
		[Bindable]
		public function get showSelectionColumn():Boolean {
			return _showSelectionColumn;
		}
		public function set showSelectionColumn(value:Boolean):void {
			_showSelectionColumn = value;
			applyColumns();
		}
		
		/**
		 * 
		 */
		[Inspectable]
		[Bindable]
		public function get noResultsMessage():String {
			return _noResultsMessage;
		}
		public function set noResultsMessage(value:String):void {
			_noResultsMessage = value;
		}
		
		/**
		 * 
		 */
		[Inspectable]
		[Bindable]
		public function get loadingErrorMessage():String {
			return _loadingErrorMessage;
		}
		public function set loadingErrorMessage(value:String):void {
			_loadingErrorMessage = value;
		}
		
		/**
		 * 
		 */
		[Inspectable]
		[Bindable]
		public function get sort():Sort {
			if (_pagedDataProvider != null)
				return _pagedDataProvider.sort;
			else
				return _sort;
		}
		public function set sort(value:Sort):void {
			_sort = value;
			if (_pagedDataProvider != null) {
				_pagedDataProvider.sort = _sort;
				if (_initialized)
					_pagedDataProvider.refresh();	
			}
		}
		
		/**
		 * 
		 */
		[Inspectable]
		[Bindable(event="dataProviderChanged")]
		public function get dataProvider():Object {
			return _rawDataProvider;
		}
		public function set dataProvider(value:Object):void {
			unsetDataProvider();
			if (value == null) {
				_pagedDataProvider = null;
			} else if (value is DataProvider) {
				_pagedDataProvider = value as DataProvider;
			} else {
				_pagedDataProvider = new LocalDataProvider(value);
			}
			_rawDataProvider = value;
			dataGrid.dataProvider = _pagedDataProvider;
			setDataProvider();
			
			dispatchEvent(new Event("dataProviderChanged"));
		}
		
		/**
		 * 
		 */
		[Inspectable]
		[Bindable(event="dataProviderChanged")]
		public function get pagedDataProvider():DataProvider {
			return _pagedDataProvider;
		}
		public function set pagedDataProvider(value:DataProvider):void {
			unsetDataProvider();
			_pagedDataProvider = value;
			_rawDataProvider = value;
			dataGrid.dataProvider = _pagedDataProvider;
			setDataProvider();
			
			dispatchEvent(new Event("dataProviderChanged"));
		}
		
		/**
		 * 
		 */
		[Inspectable]
		[Bindable]
		[ArrayElementType("com.trg.flex.dt.Column")]
		public function get columns():Array {
			return _columns;
		}
		public function set columns(value:Array):void {
			if (_columns != null) {
				for (var i:int = 0; i < _columns.length; i++) {
					Column(_columns[i]).dataTable = null;
				}
			}
			_columns = (value == null) ? [] : [].concat(value);
			applyColumns();
			if (_columns != null) {
				for (i = 0; i < _columns.length; i++) {
					Column(_columns[i]).dataTable = this;
				}
			}
		}
		
		/**
		 * A pager may be specified. It will be displayed at the bottom of the DataTable.
		 */
		[Inspectable]
		[Bindable]
		public function get pager():IPager {
			return _pager;
		}
		public function set pager(value:IPager):void {
			if (_pager != null)
				this.removeChild(_pager as DisplayObject);
			_pager = value;
			if (value != null) {
				_pager.dataProvider = _pagedDataProvider;
				this.addChild(_pager as DisplayObject);
			}
		}
		
		/**
		 * <p>size-from-results: Whenever the data changes the DataTable is automatically
		 * resized to exactly fit all the rows. If the data provider is paged, the DataTable 
		 * will be sized to fit the number of results on the current page.
		 * 
		 * <p>size-from-paging: The number of rows displayed by the DataTable is automatically
		 * adjusted to the number of rows per page on the paged data provider.
		 * 
		 * <p>paging-from-size: The results per page on the data provider is automatically updated
		 * to match the number of rows that are visible at one time in the DataTable.
		 * 
		 * <p>NOTE: We are considering breaking this into two properties, autoSize and autoPaging.
		 */
		[Bindable]
		[Inspectable(enumeration="size-from-results,size-from-paging,paging-from-size,none", defaultValue="on-screen")]
		public function get autoSize():String {
			return _autoSize;
		}
		public function set autoSize(value:String):void {
			switch (value) {
				case "size-from-results":
				case "size-from-paging":
				case "paging-from-size":
				case "none":
					break;
				default:
					return;
			}
			_autoSize = value;
		}
				
	//Selection
		protected var _selectedItem:*;
		protected var _selectedItems:Array;
		
		/**
		 * The index (zero-based) of the currently selected row if it is on screen. -1 if no
		 * on-screen row is selected. If preserveSelection is set to “off-screen” it is
		 * possible for selectedItem to be non-null but selectedIndex to be -1. This is a
		 * read-write property. If it is written, all off-screen selection will be cleared.
		 */
		[Bindable(event="change")]
		public function get selectedIndex():int {
			return dataGrid.selectedIndex;
		}
		public function set selectedIndex(value:int):void {
			dataGrid.selectedIndex = value;
			dataGrid.selectedIndices = value == -1 ? [] : [value];
			getSelectionFromDataGrid();
		}
				
		/**
		 * An array of the indices of the selected rows that are on-screen. If preserveSelection
		 * is set to “off-screen” it is possible for selectedItems to contain items that are not
		 * contained in this array of indices. This is a read-write property. If it is written,
		 * all off-screen selection will be cleared.
		 */
		[Bindable(event="change")]
		public function get selectedIndices():Array {
			return dataGrid.selectedIndices
		}
		public function set selectedIndices(value:Array):void {
			dataGrid.selectedIndices = value;
			getSelectionFromDataGrid();
		}
				
		/**
		 * Item associated with the currently selected row even if that row is off screen,
		 * which may happen if preserveSelection == “off-screen”.  (see preserveSelection).
		 * This is a read-write property.
		 */
		[Bindable(event="change")]
		public function get selectedItem():* {
			return _selectedItem;
		}
		public function set selectedItem(value:*):void {
			_selectedItem = value;
			_selectedItems = value == null ? null : [value];
			setSelectionToDataGrid();
		}
		
		/**
		 * An array of the items associated with all the selected rows, even if some or all
		 * of those rows are off screen, which may happen if preserveSelection == “off-screen”.
		 * (see <code>preserveSelection</code>). This is a read-write property.
		 * 
		 * @see #preserveSelection
		 */
		[Bindable(event="change")]
		public function get selectedItems():Array {
			if (_selectedItems == null || _selectedItems.length == 0) return [];
			return FlexUtils.copyArray(_selectedItems);
		}
		public function set selectedItems(value:Array):void {
			if (value == null) value = [];
			_selectedItem = (value.length == 0) ? null : value[0];
			_selectedItems = FlexUtils.copyArray(value);
			setSelectionToDataGrid();
		}
		
		/**
		 * <p>Used when applying selectedItems. If it is specified,items in the list
		 * will be compared by this property to the items that are indicated for
		 * selection. If it is null or unspecified, the items will be compared directly.
		 * This is used by set selectedItem, set selectedItems and preserveSelection.
		 * This can be a nested property.
		 * 
		 * <p>Here’s an example. If we have a list of items in the DataTable,
		 * <code>[{name : “Bob”}, {name : “Joe”}, {name : “Mandie”}]</code> and we
		 * call <code>dataTable.selectedItem = {name : “Joe”}</code>, no item will be
		 * selected because the “Joe” object in the list and the “Joe” object we are
		 * setting have the same values but are not actually the self same object. 
		 * But if we specify <code>dataTable.compareField = “name”<code/> first.
		 * <code>{name : “Joe”}</code> will be selected in the DataTable, because
		 * <code>“Joe” == “Joe”</code> is true in ActionScript.
		 */
		[Bindable]
		public var compareField:String;
		
		protected var _preserveSelection:String = "on-screen";
		/**
		 * If “none”, the selection will be cleared when the data is refreshed (sorted, paged,
		 * reloaded). If “on-screen”, items will remain selected that remain in the list after
		 * a refresh. If some selected items move off screen because the list is paged, they will
		 * no longer be selected. This is the default. If “off-screen”, the selection will be
		 * maintained even if items move out of the currently displayed list or even if items are
		 * deleted from the list. In this case, selectedItems may contain items that are not seen
		 * in the list.
		 */
		[Bindable]
		public function get preserveSelection():String {
			return _preserveSelection;
		}
		[Inspectable(enumeration="on-screen,off-screen,none", defaultValue="on-screen")]
		public function set preserveSelection(value:String):void {
			switch (value) {
				case "on-screen":
				case "off-screen":
				case "none":
					break;
				default:
					return;
			}
			_preserveSelection = value;
		}
		
		protected function getSelectionFromDataGrid():void {
			if (_preserveSelection == "off-screen" && _dataCursor != null && _selectedItems != null && _selectedItems.length > 0) {
				_dataCursor.seek(CursorBookmark.FIRST, -1);
				var indicies:Array = dataGrid.selectedIndices;
				var i:int = 0;
				while (_dataCursor.moveNext()) {
					var item:* = _dataCursor.current;
					var j:int = FlexUtils.indexOf(_selectedItems, item, compareField);
					if (indicies.indexOf(i) != -1) { //item is selected
						if (j == -1) //if it is not in selected items, add it
							_selectedItems.push(item);
					} else { //item is not selected
						if (j != -1) //if it is selected items, remove it
							_selectedItems.splice(j, 1);
					}
					i++;
				}
			} else {
				_selectedItem = dataGrid.selectedItem;
				_selectedItems = dataGrid.selectedItems;
			}
		}
		
		protected function setSelectionToDataGrid():void {
			if (_dataCursor == null) return;
			var indicies:Array = [];
			if (_selectedItems != null) {
				for (var i:int = _selectedItems.length - 1; i >= 0; i--) {
					var pos:int = FlexUtils.indexOf(_dataCursor, _selectedItems[i], compareField);
					if (pos != -1) {
						indicies.unshift(pos);
					} else if (_preserveSelection != "off-screen") {
						_selectedItems.splice(i,1);
					}
				}
			}
			dataGrid.selectedIndices = indicies;
		}
		
		
		protected function selectionChanged(event:Event):void {
			getSelectionFromDataGrid();
		}
		
		protected function afterCollectionChange(event:Event):void {
			if (_preserveSelection != "none") {
				setSelectionToDataGrid();
			}
		}
		
		override public function get errorString():String {
			return dataGrid.errorString;
		}
		override public function set errorString(value:String):void {
			dataGrid.errorString = value;	
		}
		
	//CONAINER
		override public function set autoLayout(value:Boolean):void { };
		
		override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void {
			super.updateDisplayList(unscaledWidth, unscaledHeight);
			
			var pagerHeight:Number = _pager == null ? 0 : _pager.measuredHeight;
			var dataGridHeight:Number = unscaledHeight - pagerHeight;
			var tempHeight:Number;
			
			if (_autoSize == AUTOSIZE_PAGING_FROM_SIZE) {
				var rows:int = dataGridHeightToRows(dataGridHeight);
				if (rows < 1) {
					rows = 1;
				} else {
					dataGridHeight = rowsToDataGridHeight(rows);
				}
				if (_pagedDataProvider != null && _pagedDataProvider.resultsPerPage != rows) {
					_pagedDataProvider.resultsPerPage = rows;
					_pagedDataProvider.refresh();
				}
			} else if (_autoSize == AUTOSIZE_SIZE_FROM_PAGING && _pagedDataProvider != null && _pagedDataProvider.resultsPerPage > 0) {
				tempHeight = rowsToDataGridHeight(_pagedDataProvider.resultsPerPage);
				if (tempHeight < dataGridHeight) dataGridHeight = tempHeight;
			} else if (_autoSize == AUTOSIZE_SIZE_FROM_RESULTS) {
				var rowCount:int = _pagedDataProvider == null ? 0 : _pagedDataProvider.length;
				tempHeight = rowsToDataGridHeight(rowCount);
				if (tempHeight < dataGridHeight) dataGridHeight = tempHeight;
			}
						
			dataGrid.setActualSize(unscaledWidth, dataGridHeight);
			
			if (_pager != null) {
				_pager.setActualSize(unscaledWidth, pagerHeight);
				_pager.move(0, dataGridHeight);
			}
		}
		
		override protected function measure():void {
			var pagerHeight:Number = _pager == null ? 0 : _pager.measuredHeight;
			var pagerWidth:Number = _pager == null ? 0 : _pager.measuredWidth;
			var dataGridHeight:Number = dataGrid.measuredHeight;
			
			if (_autoSize == AUTOSIZE_SIZE_FROM_PAGING && _pagedDataProvider != null && _pagedDataProvider.resultsPerPage > 0) {
				dataGridHeight = rowsToDataGridHeight(_pagedDataProvider.resultsPerPage);
			} else if (_autoSize == AUTOSIZE_SIZE_FROM_RESULTS) {
				var rows:int = _pagedDataProvider == null ? 0 : _pagedDataProvider.length;
				dataGridHeight = rowsToDataGridHeight(rows);
			}
			
			measuredHeight = dataGridHeight + pagerHeight;
			measuredWidth = Math.max(dataGrid.measuredWidth, pagerWidth);
		}
		
		protected function dataGridHeightToRows(height:Number):int {
			var rows:Number = height - dataGrid.headerHeight
							- dataGrid.borderMetrics.top - dataGrid.borderMetrics.bottom;
			rows /= dataGrid.measureHeightOfItems(-1,1);
			return Math.floor(rows);
		}
		
		protected function rowsToDataGridHeight(rows:int):Number {
			var height:Number = dataGrid.measureHeightOfItems(-1,1) * rows;
			height += dataGrid.headerHeight + dataGrid.borderMetrics.top + dataGrid.borderMetrics.bottom;
			return height;
		}
		
		protected function recalcAutoSizeForResults(event:Event=null):void {
			if (_autoSize != AUTOSIZE_SIZE_FROM_RESULTS) return;
			this.invalidateSize();
		}
		protected function recalcAutoSizeForPaging(event:Event=null):void {
			if (_autoSize != AUTOSIZE_SIZE_FROM_PAGING) return;
			this.invalidateSize();
		}
		
		public function get editable():Boolean {
			return dataGrid.editable;
		}
		public function set editable(value:Boolean):void {
			dataGrid.editable = value;
		}
		
	}
}